/*
 * Parser for Lisp data structures into a Java tree, functions to inspect the tree.
 */
package cz.cuni.amis.pogamut.episodic.lispbots;

/**
 * @author Zdenek Behan
 */

import java.io.*;
import java.util.*;
import java.util.regex.Pattern;

interface EventProcessor {
    boolean openNode(String goal, ArrayList<String> trace, ArrayList<String> affordances);
    boolean closeNode(String goal, ArrayList<String> trace);
    boolean atomicAction(String action, ArrayList<String> trace, ArrayList<String> affordances);
}

class dataPrinter {
    static void printData(ArrayList<String> data, PrintWriter out) throws java.io.IOException {
        Iterator<String> iter = data.listIterator();

        if (out == null)
            return;

        out.write(iter.next());
        if (iter.hasNext()) {
            out.write('(');
            out.write(iter.next());

            while (iter.hasNext()) {
                out.write(',');
                out.write(iter.next());
            }
            out.write(')');
        }
    }
}

class TraceVisitor implements Visitor<ArrayList<String>> {
    public ArrayList<ArrayList<String>> trace;
    public PrintWriter out;

    TraceVisitor(ArrayList<ArrayList<String>> _trace, ArrayList<String> data, PrintWriter fout) {
        if (_trace == null) {
            this.trace = new ArrayList<ArrayList<String>>();
        } else {
            this.trace = new ArrayList<ArrayList<String>>(_trace);
        }

        if (data != null) {
            // Or node
            this.trace.add(data);
            if (!data.get(0).substring(0,1).matches("!")) {
                ArrayList<String> orNode = new ArrayList<String>();
                orNode.add(String.format("AND_%s", data.get(0)));
                this.trace.add(orNode);
            }
        }

        out = fout;
    }

    public Visitor<ArrayList<String>> visitTree(VisitableTree<ArrayList<String>> tree) {
        return new TraceVisitor(trace, tree.showdata(), out);
    }

    public boolean visitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        ListIterator<ArrayList<String>> iter = trace.listIterator();

        if (iter.hasNext()) {
            ArrayList<String> elem = iter.next();

            while(iter.hasNext()) {
                dataPrinter.printData(elem, out);
                out.write('-');
                elem = iter.next();
            }
            dataPrinter.printData(elem, out);
            out.write('\n');
        }

        return true;
    }

    public boolean postVisitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        visitData(parent, data, isLeaf);
        return true;
    }
}

class TraceToEventVisitor extends TraceVisitor {
    ArrayList<EventProcessor> proc;

    // Constructor for ArrayList of processors
    TraceToEventVisitor(ArrayList<ArrayList<String>> _trace, ArrayList<String> data, PrintWriter fout, ArrayList<EventProcessor> _proc) {
        super(_trace, data, fout);
        proc = new ArrayList<EventProcessor>(_proc);
    }

    // Constructor for just 1 processor
    TraceToEventVisitor(ArrayList<ArrayList<String>> _trace, ArrayList<String> data, PrintWriter fout, EventProcessor _proc) {
        super(_trace, data, fout);
        proc = new ArrayList<EventProcessor>();
        proc.add(_proc);
    }

    public Visitor<ArrayList<String>> visitTree(VisitableTree<ArrayList<String>> tree) {
        return new TraceToEventVisitor(trace, tree.showdata(), out, proc);
    }

    public boolean visitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        // Name of current node, item 0
        String name = data.get(0);

        // List of affordances for current node, items 1..N
        ArrayList<String> affordances = new ArrayList<String>();
        affordances.addAll(data.subList(1, data.size()));
        //out.write(String.format("%s\nAffordances:", name));
        //int i;
        //for(i = 1; i < data.size(); i++) {
        //    out.write(String.format("%s", data.get(i)));
        //}
        //out.write("\n");

        ListIterator<EventProcessor> it = proc.listIterator();
        boolean ret = true;
        if (!isLeaf) {
            // Not a leaf = goal
            while (it.hasNext()) {
                ret &= it.next().openNode(name, nameTrace(), affordances);
            }
            return ret;
        } else {
            // leaf = Atomic action
            // TODO: hardcoded time is bad
            while (it.hasNext()) {
                ret &= it.next().atomicAction(name, nameTrace(), affordances);
            }
            return ret;
        }
    }

    public boolean postVisitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        // Name of current node, item 0
        String name = data.get(0);

        ListIterator<EventProcessor> it = proc.listIterator();
        boolean ret = true;
        if (!isLeaf) {
            while (it.hasNext()) {
                ret &= it.next().closeNode(name, nameTrace());
            }
        }
        // NOTE: No closing for atomic actions

        return true;
    }

    private ArrayList<String> nameTrace() {
        // A trace of goal names, no affordances
        ArrayList<String> nameTrace = new ArrayList<String>();

        ListIterator<ArrayList<String>> iter = trace.listIterator();
        if (iter.hasNext()) {
            ArrayList<String> elem = iter.next();

            while(iter.hasNext()) {
                nameTrace.add(elem.get(0));
                elem = iter.next();
            }
            // NOTE: The last element is the current one, do not add.
        }
        return nameTrace;
    }
}

class WriteIndentedVisitor implements Visitor<ArrayList<String>> {

    private final int indent;
    private PrintWriter out;

    WriteIndentedVisitor(int indent, PrintWriter fout) {
        this.indent = indent;
        this.out = fout;
    }

    public Visitor<ArrayList<String>> visitTree(VisitableTree<ArrayList<String>> tree) {
        return new WriteIndentedVisitor(indent + 2, out);
    }

    public boolean visitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        for (int i = 0; i < indent; i++) { // TODO: naive implementation
            out.write(" ");
        }

        dataPrinter.printData(data, out);
        out.write('\n');
        return true;
    }

    public boolean postVisitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        return true;
    }
}

class IntoSetVisitor implements Visitor<ArrayList<String>> {
    private Set itemset;
    private PrintWriter out;

    IntoSetVisitor(Set result) {
        this.itemset = result;
    }

    public Visitor<ArrayList<String>> visitTree(VisitableTree<ArrayList<String>> tree) {
        return new IntoSetVisitor(itemset);
    }

    public boolean visitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        Iterator it = data.iterator();
        itemset.add(it.next());
        return true;
    }

    public boolean postVisitData(VisitableTree<ArrayList<String>> parent, ArrayList<String> data, boolean isLeaf) throws java.io.IOException {
        return true;
    }
}

public class LispTree {
    ArrayList<String> rootNode;
    private String fileName;
    private boolean hideInternal = true;

    /**
     * Constructor
     * @param aFileName File name to initialize from
     */
    public LispTree(String aFileName) {
        fileName = new String(aFileName);
        initTreeParser();
    }

    /**
     * Constructor
     * @param aFileName File name to initialize from
     * @param hideInternal true if internal goals should be hidden
     */
    public LispTree(String aFileName, boolean aHideInternal) {
        fileName = new String(aFileName);
        hideInternal = aHideInternal;
        initTreeParser();
    }

    private void initTreeParser() {
        rootNode = new ArrayList<String>();
        // Since the top-level goal is generally a hidden goal, let's add
        // one fake top-level goal of our own to keep one day events isolated
        // properly.
        rootNode.add("_TopParent");
    }

    public void walkInForest(Visitor<ArrayList<String>> visitor) {
        VisitableTree<ArrayList<String>> forest;
        Scanner input;
        try {
            input = new Scanner(new BufferedReader(new FileReader(fileName)));
            // By delimiting using an empty line, we split the file into days.
            input.useDelimiter(Pattern.compile("^$", Pattern.MULTILINE));

            boolean rv = true;
            while (rv) {
                forest = new VisitableTree<ArrayList<String>>(rootNode, true);
                rv = processDay(forest, input);
                if (rv)
                    rv &= forest.walk(visitor);
            }
            //out.flush();
        } catch (IOException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }

    /**
     * Series of processing functions. They accept subtree, input string and
     * starting position as arguments, and return the first position after
     * processing locally has ended.
     */

    /**
     * Process a single input day. Input file is quite simply a sequence of
     * atoms, plus whitespace in between.
     *
     * @param node Parent of all top-level goals.
     * @param text String representation of the file.
     * @return Whether any data has been processed
     */
    private boolean processDay(VisitableTree<ArrayList<String>> node, Scanner input) {
        int newPos, curPos = 0;

        if (input.hasNext()) {
            String text = input.next();

            // Look at which comes first: closing or opening parenthesis
            newPos = findParentheses(text, curPos);

            // We should never encounter ')', as they have to be eaten by the
            // recursively called functions.
            if ((newPos == -1) || (text.charAt(newPos) == ')')) {
                return false;
                // TODO: Throw an exception
            }

            curPos = processAtom(node, text, newPos);
            return true;
        }
        return false;
    }

    /**
     * Process a tree atom (either goal or atomic action).
     *
     * @param node Parent
     * @param text Input string
     * @param pos Starting position
     * @return Position after atom is parsed
     */
    private int processAtom(VisitableTree<ArrayList<String>> node, String text, int pos) {
        assert(text.charAt(pos) == '(');

        // A goal will always start as "((", action always as "(!"
        if(text.charAt(pos+1) == '(') { // goal
            return processGoal(node, text, pos);
        } else if (text.charAt(pos+1) == '!') { // action
            return processAction(node, text, pos);
        } else { // neither - err out
            return -1; // TODO: Throw an exception
        }
    }

    /**
     * Process a goal.
     * @param node Parent
     * @param text Input string
     * @param pos Starting position
     * @return Position after goal is parsed
     */
    private int processGoal(VisitableTree<ArrayList<String>> node, String text, int pos) {
        assert(text.charAt(pos) == '(');
        assert(text.charAt(pos+1) == '(');

        int newPos, curPos = pos;

        VisitableTree<ArrayList<String>> goalNode;

        // A Syntax of a goal is:
        // ((goal descriptor) (atom1) (atom2) ... (atomn))

        // First process the goal descriptor
        String s = processAtomic(text, pos+1);
        ArrayList<String> newNode = new ArrayList<String>();
        if (hideInternal && (s.charAt(0) == '_')) {
            goalNode = node; // For internal goals, have the parent inherit our children
        } else {
            for (String str : s.split(" ")) {
                newNode.add(str);
            }
            goalNode = node.child(newNode);
        }

        curPos += s.length()+3; // add "(()"

        while (curPos != text.length()) {
            // Look at which comes first: closing or opening parenthesis
            newPos = findParentheses(text,curPos);

            if (newPos == -1)
                return text.length(); // TODO: Throw an exception

            if (text.charAt(newPos) == '(') { //
                curPos = processAtom(goalNode, text, newPos);
            } else {
                return newPos+1;
            }
        }

        return 0;
    }

    /**
     * Process an atomic action
     * @param node Parent
     * @param text Input string
     * @param pos Starting position
     * @return Position after action is parsed
     */
    private int processAction(VisitableTree<ArrayList<String>> node, String text, int pos) {
        assert(text.charAt(pos) == '(');
        assert(text.charAt(pos+1) == '!');

        String s = processAtomic(text, pos);
        ArrayList<String> newNode = new ArrayList<String>();

        // For internal actions, simply skip them
        if (!(hideInternal && (s.charAt(1) == '_'))) {
            // Skip the '!'
            for (String str : s.split(" ")) {
                newNode.add(str);
            }
            node.child(newNode);
        }

        return pos+s.length()+2; // Add the "()"
    }


///////////////////////////////////// Utility functions

    /**
     * Utility function to look for ( or ) in text.
     * @param text String to search
     * @param pos Starting position
     * @return Position of either ( or )
     */
    private static int findParentheses(String text, int pos) {
        int newPosOpen, newPosClose;

        newPosOpen = text.indexOf("(",pos);
        newPosClose = text.indexOf(")",pos);

        if (newPosClose == -1)
            newPosClose = text.length()+1;
        if (newPosOpen == -1)
            newPosOpen = text.length()+1;

        if (newPosOpen < newPosClose) { // opening first
            return newPosOpen;
        } else if (newPosOpen > newPosClose) { // closing first
            return newPosClose;
        } else { // equal = no opening or closing parenthesis
            return -1;
        }
    }

    /**
     * Utility function to process everything from a starting ( to ending )
     * @param text Input string
     * @param pos Starting position
     * @return Discovered string
     */
    private static String processAtomic(String text, int pos){
        assert(text.charAt(pos) == '(');

        int newPos = findParentheses(text, pos+1);

        if ((newPos == -1) || (text.charAt(newPos) == '('))
            return ""; // TODO: Throw an exception

        // newPos points at ')'
        return text.substring(pos+1,newPos);
    }
}
